namespace cxxcall
{

static void
cb_receiver (uintptr_t w1, uintptr_t w2, uintptr_t w3, uintptr_t w4,
  uintptr_t w5, uintptr_t w6, uintptr_t w7, uintptr_t w8, uintptr_t first,
  long double d1, long double d2, long double d3, long double d4,
  long double d5, long double d6, long double d7, long double d8)
{
#ifndef __clang__
  register cb_fct *env __asm__ ("x18");
#else
  cb_fct *env;
  __asm__ __volatile__ ("mov %0, x18" : "=r" (env));
#endif
  cb_data data;

#define I_(x)   data.iargs[x - 1] = w##x
#define F_(x)   data.fargs[x - 1].ld = d##x

  I_(1);
  I_(2);
  I_(3);
  I_(4);
  I_(5);
  I_(6);
  I_(7);
  I_(8);

  F_(1);
  F_(2);
  F_(3);
  F_(4);
  F_(5);
  F_(6);
  F_(7);
  F_(8);

  data.flags = 0;
  data.curr = (uintptr_t)&first;
  data.rtype = type::void_t;
  __asm__ __volatile__ ("str x8, %0" : "=m" (data.structaddr));
  env->fn (&data, env->arg);

  switch (data.rtype)
    {
      case type::void_t:
        break;

#define SET_REG(Name, Value)   \
  __asm__ __volatile__ ("mov " Name ", %0" : : "r" (Value) : Name)

#define SET_X0(Value)   SET_REG ("x0", ((intptr_t)(Value)))
#define SET_X1(Value)   SET_REG ("x1", ((intptr_t)(Value)))

#define IRET(Typecode, Member)   \
  case type::Typecode:   \
    SET_X0 (data.tmp.Member);   \
    break

      IRET (schar_t, sch);
      IRET (uchar_t, uch);
      IRET (sshort_t, shrt);
      IRET (ushort_t, ushrt);
      IRET (sint_t, ival);
      IRET (uint_t, uival);
      IRET (slong_t, lval);
      IRET (ulong_t, ulval);
      IRET (sllong_t, llval);
      IRET (ullong_t, ullval);

      case type::float_t:
        __asm__ __volatile__ ("ldr s0, %0" : : "m" (data.tmp.flt));
        break;

      case type::double_t:
        __asm__ __volatile__ ("ldr d0, %0" : : "m" (data.tmp.dbl));
        break;

#ifndef CXXCALL_LONG_DOUBLE_ALIASED
      case type::ldouble_t:
        __asm__ __volatile__ ("ldr q0, %0" : : "m" (data.tmp.ldbl));
        break;
#endif

      case type::struct_t:
        if (!(data.flags & av_base::reg_struct_ret))
          break;

        {
          size_t size = data.rsize;
          auto wp = (uintptr_t *)((uintptr_t)data.raddr & ~(WSIZE - 1));
          auto start = (uintptr_t)data.raddr & (WSIZE - 1), end = start + size;

          if (size <= WSIZE)
            { // Fits in x0.
              if (end <= WSIZE)
                SET_X0 ((*wp & ((2ul << (end * 8 - 1)) - 1)) >> (start * 8));
              else
                SET_X0 (((*wp >> (start * 8)) | (wp[1] & ((2ul <<
                  (end * 8 - WBITS - 1)) - 1) << (WBITS - start * 8))));
            }
          else if (end <= 2 * WSIZE)
            {
              auto m1 = (2ul << (end * 8 - WBITS - 1)) - 1;
              auto x0 = (*wp >> (start * 8)) | ((wp[1] & m1) <<
                (WSIZE * 4 - start * 4) << (WSIZE * 4 - start * 4));
              auto x1 = (wp[1] & m1) >> (start * 8);

              SET_X0 (x0);
              SET_X1 (x1);
            }
          else
            {
              auto x0 = (*wp >> (start * 8)) | (wp[1] << (WBITS - start * 8));
              auto x1 = ((wp[1] >> (start * 8)) | ((wp[2] &
                ((2ul << (end * 8 - 2 * WBITS - 1)) - 1)) <<
                (WBITS - start * 8)));

              SET_X0 (x0);
              SET_X1 (x1);
            }
        }
    }

#undef SET_REG
#undef IRET
#undef I_
#undef F_
}

static const size_t TRAMP_LEN = 32;

void cb_data::fill (void *outp, void (*fn) (cb_data *, void *), void *argx)
{
  void *fct = (void *)cb_receiver;
  void **arg = (void **)((char *)outp + 32);
  const unsigned int ops[] = { 0x580000D1, 0x58000072, 0xD61F0220, 0xD503201F };

  memcpy (outp, ops, sizeof (ops));
  outp = (char *)outp + sizeof (ops);
  ((uintptr_t *)outp)[0] = (uintptr_t)arg;
  ((uintptr_t *)outp)[1] = (uintptr_t)fct;

  arg[0] = (void *)fn, arg[1] = argx;
}

} // namespace cxxcall
